#!/bin/bash
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[[ -n ${gbmc_nic_neigh_lib-} ]] && return

# shellcheck source=meta-google/recipes-google/networking/network-sh/lib.sh
source /usr/share/network/lib.sh || exit
# shellcheck source=meta-google/recipes-google/networking/gbmc-net-common/gbmc-net-lib.sh
source /usr/share/gbmc-net-lib.sh || exit

gbmc_nic_neigh_intfs=(@IFS@)
gbmc_nic_neigh_addr=

gbmc_nic_neigh_set() {
  local act="$1"
  local ip="$2"

  echo "gBMC NIC Neigh $act $ip: ${gbmc_nic_neigh_intfs[*]}" >&2

  local pfx_bytes=()
  ip_to_bytes pfx_bytes "$ip"
  (( pfx_bytes[8] |= 0xfd ))
  local neigh_ips=("$ip")
  local i
  for (( i=1; i<16; ++i )); do
    (( pfx_bytes[9] &= 0xf0 ))
    (( pfx_bytes[9] |= i ))
    neigh_ips+=("$(ip_bytes_to_str pfx_bytes)")
  done

  local intf
  local failed_intfs=()
  for intf in "${gbmc_nic_neigh_intfs[@]}"; do
    local rt
    rt="$(gbmc_net_route_table_for_intf "$intf")" || continue
    local contents
    read -r -d '' contents <<EOF
[RoutingPolicyRule]
To=$ip/64
Table=main
Priority=$GBMC_INTF_ROUTE_TABLE_BASE
[RoutingPolicyRule]
From=$ip/64
Table=$rt
Priority=$rt
[Network]
IPv6ProxyNDP=yes
EOF
    local neigh_ip
    for neigh_ip in "${neigh_ips[@]}"; do
      contents+=$'\n'"IPv6ProxyNDPAddress=$neigh_ip"
    done
    # Override any existing gateway information within files
    # Make sure we cover `00-*` and `-*` files
    for file in /run/systemd/network/{00,}-bmc-"$intf".network; do
      mkdir -p "$file.d"
      if [[ "$act" == add ]]; then
        printf '%s' "$contents" >"$file.d"/10-nic-neigh-table.conf
      else
        rm -f "$file.d"/10-nic-neigh-table.conf
      fi
    done

    local st=0
    ip -6 rule del to "$ip/64" pref "$GBMC_INTF_ROUTE_TABLE_BASE" lookup main 2>dev/null || true
    ip -6 rule del from "$ip/64" pref "$rt" lookup "$rt" 2>/dev/null || true

    if [[ "$act" == add ]]; then
      sysctl net.ipv6.conf."$intf".proxy_ndp=1 >/dev/null || st=$?
      for neigh_ip in "${neigh_ips[@]}"; do
        ip -6 neigh "$act" proxy "$neigh_ip" dev "$intf" || st=$?
      done
      ip -6 rule add to "$ip/64" pref "$GBMC_INTF_ROUTE_TABLE_BASE" lookup main || st=$?
      ip -6 rule add from "$ip/64" pref "$rt" lookup "$rt" || st=$?
    fi
    [[ "$st" == "0" ]] || failed_intfs+=("$intf")

    # Write the necessary firewall rules to forward IP traffic
    if [[ "$act" == add ]]; then
      cat >/run/nftables/40-gbmc-"$intf"-br.rules <<EOF
table inet filter {
  chain ${intf}_forward {
    ip6 saddr != $ip/64 ip6 daddr $ip/64 accept
  }
}
EOF
    else
      rm -f /run/nftables/40-gbmc-"$intf"-br.rules
    fi
  done
  systemctl reset-failed nftables || true
  systemctl --no-block reload-or-restart nftables || true
  if (( "${#failed_intfs[@]}" > 0 )); then
    gbmc_net_networkd_reload "${failed_intfs[@]}"
  fi
}

gbmc_nic_neigh_hook() {
  # shellcheck disable=SC2154
  if [[ $change == addr && $intf == gbmcbr && $scope == global ]] &&
       [[ $fam == inet6 && $flags != *tentative* ]]; then
    local ip_bytes=()
    if ! ip_to_bytes ip_bytes "$ip"; then
      echo "gBMC Bridge Ensure RA Invalid IP: $ip" >&2
      return 1
    fi
    # Ignore ULAs
    if (( (ip_bytes[0] & 0xfe) == 0xfc )); then
      return 0
    fi
    # Addresses must be /64 to the upstack switch
    for (( i = 8; i < 16; ++i )); do
      if (( ip_bytes[i] != 0 )); then
        return 0
      fi
    done
    if [[ $action == add && "$gbmc_nic_neigh_addr" != "$ip" ]]; then
      if [ -n "$gbmc_nic_neigh_addr" ]; then
        gbmc_nic_neigh_set del "$gbmc_nic_neigh_addr"
      fi
      gbmc_nic_neigh_addr="$ip"
      gbmc_nic_neigh_set add "$ip"
    elif [[ $action == del && "$gbmc_nic_neigh_addr" == "$ip"  ]]; then
      gbmc_nic_neigh_addr=
      gbmc_nic_neigh_set del "$ip"
    fi
  elif [[ $change == link && $action == add && $carrier == UP && -n $gbmc_nic_neigh_addr ]]; then
    local gbmcintf
    for gbmcintf in "${gbmc_nic_neigh_intfs[@]}"; do
      if [[ $intf == "$gbmcintf" ]]; then
        ip neigh replace proxy "$gbmc_nic_neigh_addr" dev "$intf"
      fi
    done
  fi
}

GBMC_IP_MONITOR_HOOKS+=(gbmc_nic_neigh_hook)

gbmc_nic_neigh_lib=1
